# forward-compat boilerplate
from __future__ import absolute_import
from __future__ import with_statement
__metaclass__ = type

import re
import sys
import decimal
import datetime

import pytz

try:
    import json
except:
    import simplejson as json

from fenton import util
from fenton import widgets
from fenton import timestamp


# TODO: date format configuration
def format_date(value):
    d = value.day
    a = value.strftime('%a')
    return '%d %s' % (d, value.strftime('%b %Y'))
    return '%s, %d %s' % (a, d, value.strftime('%b, %Y'))


def format_time(value):
    secs = value.second and ':%S' or ''
    return value.strftime('%H:%M' + secs)


def format_days(d):
    y = int(d / 365.25)
    if y > 1:
        return str(y) + ' years'
    m = int(d / 30.4)
    if m > 1:
        return str(m) + ' months'
    w = int(d / 7.0)
    if w > 1:
        return str(w) + ' weeks'
    return str(d) + ' days'


def as_sequence(value):
    if value is None:
        return []
    if isinstance(value, basestring):
        return [value]
    if isinstance(value, (list, tuple)):
        return value
    try:
        for n in value:
            break
    except TypeError:
        return [value]
    else:
        return value




def localize(f):
    import functools
    @functools.wraps(f)
    def local(self):
        ts = f(self)
        return ts and ts.astimezone(self.tz)
    return local




def localdate(f):
    import functools
    @functools.wraps(f)
    def local(self):
        ts = f(self)
        return ts and ts.astimezone(self.tz).date()
    return local



def localtime(f):
    import functools
    @functools.wraps(f)
    def local(self):
        ts = f(self)
        return ts and ts.astimezone(self.tz).time()
    return local





def pair_property(name):
    return multi_property(lambda i:'%s_%d'%(name, i), 2)



class multi_property(object):
    def __init__(self, translate, size):
        self.translate = translate
        self.size = size

    def __get__(self, obj, type):
        x = obj is None and type or obj
        return [getattr(x, self.translate(i))
                for i in xrange(self.size)]

    def __set__(self, obj, value):
        for i in xrange(self.size):
            setattr(obj, self.translate(i), value[i])




class Invalid(Exception):
    message = None
    def __init__(self, message=None, valid=None, errors=None, input=None):
        if message:
            self.message = message
        self.valid = valid
        self.errors = errors
        self.input = input
        # unlike normal objects, exceptions are
        # unpickled by calling the constructor with whatever
        # is in the 'args' attribute
        # therefore, we must set this attribute
        self.args = message, valid, errors, input

    def get_message(self):
        return self.message or util.decamel(self.__class__.__name__).replace('_', ' ').title()

    def __str__(self):
        return self.get_message()

    def __unicode__(self):
        msg = self.get_message()
        if isinstance(msg, unicode):
            return msg
        elif isinstance(msg, str):
            return msg.decode('utf8')
        else:
            return unicode(msg)




class NoSuchOption(Invalid):
    pass




class ListErrors(Invalid):
    pass




class Type:
    # static type configuration
    view_widget = widgets.string_view
    edit_widget = widgets.string_edit
    multi_view_widget = widgets.simple_list_view
    multi_edit_widget = widgets.simple_list_edit

    # instance vars
    showview = True
    showedit = True
    editable = True
    required = True
    hint = None
    label = None
    default = None
    meta = None
    key = None
    widget_config = None
    num_args = 1
    thaw = False

    def __init__(self, impl=None, **kw):
        self.impl = impl
        self.__dict__.update(kw)
        if not self.showedit:
            self.editable = False
        self.widget_config = self.widget_config or {}

    def __call__(self, impl):
        'decorator interface; impl is property'
        if self.impl is not None:
            raise TypeError('impl %s already supplied' % self.impl)
        self.impl = impl
        return self

    def instrument(self, Class, name):
        setattr(Class, name, self.impl)

    def set_owner(self, meta, key):
        self.owner = meta
        self.key = key
        self.resolve()

    def get_value(self, context, obj, key):
        return getattr(obj, key)

    def resolve(self):
        pass

    def get_default(self, obj):
        return self.default

    def get_label(self):
        return self.label or self.key.replace('_', ' ').capitalize()

    def get_hint(self):
        return self.hint

    def get_validator(self):
        return self

    def get_widget(self, field):
        args = self.get_widget_args(field)
        return self.get_widget_class(field.editable)(None, field=field, **args)

    def get_widget_args(self, field):
        return dict(**self.widget_config)

    def get_widget_class(self, showedit):
        return showedit and self.edit_widget or self.view_widget

    def get_multi_widget_class(self, showedit):
        return showedit and self.multi_edit_widget or self.multi_view_widget

    def send_thaw(self, context, obj):
        pass

    def format_value(self, value, context):
        if value is None:
            return self.format_empty(value, context)
        else:
            return self.format_nonempty(value, context)

    def format_empty(self, value, context):
        return u''

    def format_nonempty(self, value, context):
        return unicode(value)

    def is_equal(self, l, r):
        return l == r

    def simplify(self, value, context):
        'convert python object to string or serializable'
        if value is None:
            return ''
        return unicode(value)

    def reconstruct(self, value, context):
        'convert string or serializable to python object'
        if value == '':
            return None
        return value




class Primitive(Type):
    'wrapper for python primitive types'




class Lookup(Type):
    other_key = False
    other_label = 'Other...'
    option_filter = None
    def get_options(self, field):
        opts = [x[0] for x in self.options]
        owner = field.form.model
        if self.option_filter:
            if isinstance(self.option_filter, basestring):
                filter = getattr(field.form.model, self.option_filter)
            else:
                filter = lambda q: self.option_filter(owner, q)
            opts = filter(opts)

        for x in opts:
            yield x, x




class ResolutionError(Exception):
    pass




class AbstractReference(Type):
    thaw = True
    thaw_depends = None
    def __init__(self, model_class, impl=None, **kw):
        self.model_class = model_class
        self.meta = None
        Type.__init__(self, impl, **kw)

    def __repr__(self):
        n = self.__class__.__name__
        return '<%s meta=%s>' % (n, self.meta)

    def resolve(self):
        from fenton import getmeta
        if isinstance(self.model_class, type(lambda:0)):
            self.model_class = self.model_class()
        elif isinstance(self.model_class, basestring):
            name = self.model_class
            if name in self.owner.meta_map:
                self.model_class = self.owner.meta_map[name].model_class
            elif name in self.owner.class_map:
                self.model_class = self.owner.class_map[name]
            else:
                module = sys.modules[self.owner.model_class.__module__]
                if '.' in name:
                    module, name = util.get_modattr(name)
                try:
                    self.model_class = getattr(module, name)
                except AttributeError, e:
                    c = self.owner.model_class
                    msg = '"%s" in %s.%s::%s' % (name, c.__module__, c.__name__, self.key)
                    raise ResolutionError(msg)
        self.meta = getmeta(self.model_class)

    def send_thaw(self, context, obj):
        from fenton import getmeta
        if self.thaw:
            if self.thaw_depends:
                for k in self.thaw_depends:
                    getmeta(obj).attributes[k].send_thaw(context, obj)
            x = getattr(obj, self.key, None)
            if x is not None:
                getmeta(x).thaw_model(context, x)




class Reference(AbstractReference, Lookup):
    option_filter = None
    view_widget = widgets.reference_view
    edit_widget = widgets.single_option_select
    multi_view_widget = widgets.reference_list_view
    multi_edit_widget = widgets.simple_select_list
    link = False #True

    num_args = property(lambda x:x.meta.num_args)

    def get_options(self, field):
        owner = field.form.model
        if self.option_filter:
            if isinstance(self.option_filter, basestring):
                filter = getattr(field.form.model, self.option_filter)
            else:
                filter = lambda q: self.option_filter(owner, q)
        else:
            filter = None
        for obj in self.meta.iter_model(field.context, filter):
            yield self.meta.get_id(field.context, obj), obj

    def simplify(self, value, context):
       return self.meta.simplify(value, context)

    def reconstruct(self, value, context):
        if not value:
            return None
        if self.other_key and value == self.other_key:
            return self.other_key
        return self.meta.reconstruct(value, context)




class Child(AbstractReference):
    view_widget = widgets.child_single_view
    edit_widget = widgets.child_single_edit
    multi_view_widget = widgets.child_list_view
    multi_edit_widget = widgets.child_list_edit
    link_column = False
    show_header = True

    def get_validator(self):
        return None




class Aggregate(Type):
    meta = property(lambda x:x.of.meta)
    def __init__(self, of, impl=None, **kw):
        if isinstance(of, type):
            of = of()
        if isinstance(of, basestring):
            of = Reference(of)
        self.of = of
        Type.__init__(self, impl, **kw)

    def is_equal(self, l, r):
        return set(l or []) == set(r or [])




class List(Aggregate):
    index_attr = None
    columns = None
    link_column = False
    show_header = True

    def __repr__(self):
        return '<list of=%s>' % self.of

    def set_owner(self, meta, key):
        self.of.set_owner(meta, key)
        self.owner = meta
        self.key = key

    def format_nonempty(self, value, context):
        if not value:
            return ''
        return ', '.join(self.of.format_nonempty(x, context)
                         for x in value)

    def get_default(self, obj):
        return list()

    def get_validator(self):
        v = self.of.get_validator()
        return v and self or None

    def get_widget_class(self, showedit):
        return self.of.get_multi_widget_class(showedit)

    def send_thaw(self, context, obj):
        if self.thaw:
            thaw = self.of.send_thaw
            xs = getattr(obj, self.key, None)
            for x in xs or []:
                thaw(context, x)

    def simplify(self, value, context):
        cons = isinstance(value, set) and set or list
        value = as_sequence(value)
        if not value:
            return []
        return cons(self.of.get_validator().simplify(item, context)
                  for item in value)

    def reconstruct(self, value, context):
        #as_set = isinstance(value, set)
        value = as_sequence(value or [])
        if not value:
            return []

        result = []
        errors = []
        for item in value:
            good = util.undefined
            try:
                good = self.of.get_validator().reconstruct(item, context)
            except Invalid, error:
                errors.append(error)
                good = error.valid
            if good is not util.undefined:
                result.append(good)
        #if as_set:
        #    result = set(result)
        if errors:
            raise ListErrors(value, context, result, errors)
        return list(set(result))




class Tuple(Aggregate):
    labels = None

    class view_widget(widgets.field_widget):
        class_id = 'tuple_view'
        template_file = 'widgets'
        labels = property(lambda x:x.field.type.labels)

    class edit_widget(widgets.field_widget):
        class_id = 'tuple_edit'
        template_file = 'widgets'
        labels = property(lambda x:x.field.type.labels)

    def format_value(self, value, context):
        values = [self.type.format_value(v, context)
                  for v in value]
        if not any(values):
            return ''
        return ', '.join(values)

    def reconstruct(self, values, context):
        return tuple(self.type.reconstruct(v.strip(), context)
                     for v in values)

    def simplify(self, values, context):
        return tuple(self.type.simplify(v, context)
                     for v in values)



class Pair(Aggregate):

    of = None
    labels = None

    @util.lazyattr
    def type(self):
        if isinstance(self.of, type):
            self.of = self.of()
        return self.of

    class view_widget(widgets.field_widget):
        class_id = 'pair_view'
        template_file = 'widgets'
        labels = property(lambda x:x.field.type.labels)

    class edit_widget(widgets.field_widget):
        class_id = 'pair_edit'
        template_file = 'widgets'
        labels = property(lambda x:x.field.type.labels)

    def format_value(self, value, context):
        values = (self.type.format_value(value[0], context),
                  self.type.format_value(value[1], context))
        if not any(values):
            return ''
        return ', '.join(values)

    def reconstruct(self, value, context):
        l, r = [v.strip() for v in value]
        return (self.type.reconstruct(l, context),
                self.type.reconstruct(r, context))

    def simplify(self, value, context):
        return (self.type.simplify(value[0], context),
                self.type.simplify(value[1], context))





class String(Primitive):
    def reconstruct(self, value, context):
        value = value.strip()
        return value and unicode(value) or None

    def simplify(self, value, context):
        return value
        #return value and unicode(value.strip()) or u''




class Text(String):
    edit_widget = widgets.textbox_edit
    view_widget = widgets.text_view




class MaskedString(String):
    view_widget = widgets.string_mask_view
    edit_widget = widgets.string_mask_edit



class Code(String):
    edit_widget = widgets.code_edit
    chars = 'A-Z0-9_.-'
    case = 'upper'
    def reconstruct(self, value, context):
        value = value.strip()
        if not value:
            return
        if self.case:
            value = getattr(value, self.case)()
        reg = re.compile('[^%s]+' % self.chars)
        return reg.sub('', value)




class Integer(Primitive):
    def reconstruct(self, value, context):
        value = value.strip()
        if not value:
            return None
        try:
            return int(value)
        except (ValueError, TypeError):
            raise Invalid('Not a number: %s' % value)




class NotANumber(Invalid):
    def get_message(self):
        return self.message or util.decamel(self.__class__.__name__).replace('_', ' ').title()




class Enum(Lookup):
    view_widget = widgets.reference_view
    edit_widget = widgets.single_option_select
    multi_view_widget = widgets.simple_list_view
    multi_edit_widget = widgets.simple_select_list
    options = None
    link = False

    def format_nonempty(self, value, context):
        return dict(self.options).get(value, value)

    def reconstruct(self, value, context):
        if not value:
            return None
        opts = [x[0] for x in self.options]
        if value not in opts and not self.other_key:
            raise Invalid('No such option: %s' % value)
        return value

    def get_default(self, obj):
        if self.default is None:
            return self.options[0][0]
        else:
            return self.default




class Boolean(Enum):
    edit_widget = widgets.boolean_edit
    options = ((True, 'Yes'), (False, 'No'))
    strvalues = [str(x[0]) for x in reversed(options)]

    def reconstruct(self, value, context):
        if not value:
            return None
        try:
            return bool(self.strvalues.index(value))
        except ValueError:
            raise Invalid('Expected True or False')




class ExpectedTrueOrFalse(Invalid):
    pass




class Number(Primitive):
    def reconstruct(self, value, context):
        value = value.strip()
        if not value:
            return None
        try:
            value = float(value)
            try:
                int_value = int(value)
            except OverflowError:
                int_value = None
            if value == int_value:
                return int_value
            return value
        except ValueError:
            raise Invalid('Not a number: %s' % value)




class Decimal(Number):
    def reconstruct(self, value, context):
        value = value.strip()
        if not value:
            return None
        value = Number.reconstruct(self, value, context)
        return decimal.Decimal(value)




class Date(Primitive):
    regexp = '\d{4}(-\d{2}){2}'
    regexp = re.compile(regexp)

    view_widget = widgets.date_view
    edit_widget = widgets.date_edit

    def format_nonempty(self, value, context):
        return format_date(value)

    def simplify(self, value, context):
        if not value:
            return ''
        return '%04d-%02d-%02d' % (value.year, value.month, value.day)

    def reconstruct(self, value, context):
        value = value.strip()
        if not value:
            return None
        if not self.regexp.match(value):
            raise Invalid('Not a date: %s' % value)
        try:
            args = map(int, value.split('-'))
            return datetime.date(*args)
        except Exception, e:
            raise Invalid(message=str(e))




class NotADate(Invalid):
    pass




class Time(Primitive):
    regexp = re.compile('^(\d\d?)([:\.]?\d\d) *([aApP][mM])?$')
    edit_widget = widgets.time_edit

    def reconstruct(self, value, context):
        if not value:
            return None
        m = self.regexp.match(value)
        if not m:
            raise Invalid
        hour, minute, meridian = m.groups()
        hour = hour and int(hour) or 0
        minute = minute and int(minute[-2:]) or 0
        meridian = meridian and meridian.upper()

        if hour > 23:
            raise Invalid('Hour out of range')
        if minute > 61:
            raise Invalid('Minute out of range')
        if meridian == 'PM' and hour < 12:
            hour += 12
        try:
            return datetime.time(hour, minute)
        except Exception, e:
            raise Invalid

    def simplify(self, value, context):
        return self.format_value(value, context)

    def format_nonempty(self, value, context):
        s = u'%02d:%02d' % (value.hour, value.minute)
        if value.second:
            s += u':%02d' % value.second
        return s




class DateTime(Primitive):
    def reconstruct(self, value, context):
        try:
            if len(value) > 16:
                return datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S')
            else:
                return datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M')
        except Exception, e:
            raise Invalid

    def simplify(self, value, context):
        if not value:
            return ''
        fmt = '%Y-%m-%dT%H:%M'
        if value.second:
            fmt += ':%S'
        return unicode(value.strftime(fmt))




class LocalDateTime(Primitive):
    view_widget = widgets.local_datetime_view
    edit_widget = widgets.local_datetime_edit

    follows_from = None
    get_tz = None

    def get_value(self, context, obj, key):
        value = getattr(obj, key)
        if not value:
            return None
        tz = self.get_tz and self.get_tz(obj)
        if tz:
            value = value.astimezone(tz)
        return value

    def simplify(self, value, context):
        if not value:
            return ''
        iso_fmt = '%Y-%m-%dT%H:%M:%S'
        z = value.strftime('%z')
        z = z[:3] + ':' + z[3:]
        return value.strftime(iso_fmt) + z

    def reconstruct(self, value, context):
        if not value:
            return None
        try:
            ts = timestamp.parse(value)
        except Exception, e:
            raise Invalid(str(e))
        if not ts.tzinfo:
            raise Invalid('No timezone')
        return ts

    def format_nonempty(self, value, context):
        d = self.format_date(value, context)
        t = self.format_time(value, context)
        #z = self.format_tz(value, context)
        z = value.strftime('%Z')
        return '%s %s %s' % (d, t, z)

    def format_date(self, value, context):
        return format_date(value)

    def format_time(self, value, context):
        return format_time(value)

    def utc_from_local():
        try:
            ts = tz.localize(ts, is_dst=None)
        except pytz.AmbiguousTimeError, e:
            raise Invalid('Ambiguous time')




class Minutes(Primitive):
    regexp = re.compile('^((\d+)\s*h)?\s*((\d+)\s*m?)$')

    def format_hhmm(self, value):
        if value == 0:
            return '0'
        sign = ''
        if value < 0:
            value = -value
            sign= '-'
        h, m = divmod(value, 60)
        return '%s%dh%02dm' % (sign, h, m)

    def format_decimal(self, value):
        if value == 0:
            return '0'
        return '%.2fh' % (value / 60.0)

    def format_nonempty(self, value, context):
        if value == 0:
            return '0'
        return self.format_hhmm(value)

    def reconstruct(self, value, context):
        value = value.strip()
        if not value:
            return None

        match = self.regexp.match(value)
        if match is None:
            raise Invalid('Value not in format (12h)34(m)', context)

        hh, h, mm, m = match.groups()
        if not any((hh, h, mm, m)):
            return 0
        return int(h or 0) * 60 + int(m or 0)

    def simplify(self, value, state):
        if value is None:
            return ''
        return self.format_hhmm(value)




class IsoWeek(Primitive):
    reg = re.compile('^(\d{4})-(\d{1,2})$')
    def reconstruct(self, value, context):
        match = self.reg.match(value)
        if not match:
            raise Invalid
        y, m = map(int, match.groups())
        if 0 < m < 54:
            return isoweek(y, m)
        raise Invalid

    def simplify(self, value, context):
        return '%d-%d' % (value.year, value.week)

    format_nonempty = simplify




class isoweek:
    def __init__(self, year, week):
        self.year = year
        self.week = week

    def __repr__(self):
        return 'isoweek<%s, %s>' % (self.year, self.week)

    @property
    def monday(self):
        # http://en.wikipedia.org/wiki/ISO_week_date#Calculation
        correction = datetime.date(self.year, 1, 4).isoweekday() + 3
        offset = datetime.timedelta(self.week * 7 - correction)
        return datetime.date(self.year, 1, 1) + offset




class Period(Primitive):
    regexp = re.compile('\d{4}(-\d{2}){2}-\d+')
    fmt = '%04d-%02d-%02d-%d'

    def reconstruct(self, value, context):
        if not self.regexp.match(value):
            raise Invalid
        try:
            y, m, d, n = map(int, value.split('-'))
            d = datetime.date(y, m, d)
        except Exception, e:
            raise Invalid
        else:
            return period(d, n)

    def simplify(self, value, context):
        t = value.first
        days = value.delta.days
        return self.fmt % (t.year, t.month, t.day, days)




class period:
    def __init__(self, first, arg):
        try:
            days = int(arg)
        except (TypeError, ValueError):
            try:
                days = arg.days
            except AttributeError:
                try:
                    days = (arg - first).days
                except (TypeError, ValueError):
                    raise TypeError('Invalid argument %r of type %r' % (arg, type(arg)))

        if days < 0:
            days = -days
            first = first - datetime.timedelta(days)
        self.first = first
        self.days = days

    def __len__(self):
        return self.days

    def __iter__(self):
        for i in range(self.days + 1):
            yield self.first + datetime.timedelta(i)

    def __getitem__(self, i):
        return list(self)[i]

    def __repr__(self):
        return 'period(%s, %d)' % (self.first, self.days)

    def __str__(self):
        return '%s -- %s' % (self.first, self.last)

    def __iadd__(self, arg):
        if isinstance(arg, int):
            arg = datetime.timedelta(arg)
        self.first += arg
        return self

    def __isub__(self, arg):
        self += -arg
        return self

    def __add__(self, arg):
        if isinstance(arg, int):
            arg = datetime.timedelta(arg)
        result = self.__class__(self.first, self.days)
        result += arg
        return result

    def __sub__(self, arg):
        return self + -arg

    def __eq__(self, value):
        if not isinstance(value, self.__class__):
            return False
        return self.first == value.first and self.days == value.days

    @property
    def delta(self):
        return datetime.timedelta(self.days)

    @property
    def last(self):
        return self.first + datetime.timedelta(self.days)





